package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;

@Component
@Slf4j
public class CacheClient {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public void set(String key, Object value, Long Time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), Time, unit);
    }




    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        // 设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        // 写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }
    public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long Time, TimeUnit unit) {
        //设置随机过期时间，防止缓存雪崩问题
        Random random = new Random();
        long expire = random.nextInt(20);


        String key = keyPrefix + id;
        //1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);


        if (StrUtil.isNotBlank(json)) {
            //2.如果缓存存在，直接返回
            //将 JSON 字符串转换成一个 Java 对象（Java Bean），也就是我们常说的 反序列化。
            R r = JSONUtil.toBean(json, type);
            return r;
        }
        //判断缓存命中是否是空值
        if (Objects.equals(json, "")) {
            return null;
        }

        //3.如果缓存不存在：
        //  1)查询数据库
        R r = dbFallback.apply(id);
        //  a.如果商铺不存在，返回错误信息
        if (r == null) {
            //解决缓存穿透问题：商铺不存在，写入空值，也将其放入redis中
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL + expire, TimeUnit.MINUTES);
            return null;
        }
        //  b.如果商铺存在，将商铺放入redis缓存中
        //将 Java 对象（Java Bean）转换成 JSON 字符串，也就是我们常说的序列化。
        this.set(key, r, Time, unit);

        //  2)返回商铺信息
        return r;
    }


    //利用互斥锁解决缓存击穿问题
    public <R, ID>  R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long Time, TimeUnit unit) {
        //设置随机过期时间，防止缓存雪崩问题
        Random random = new Random();
        long expire = random.nextInt(20);


        String key = keyPrefix + id;
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);


        if (StrUtil.isNotBlank(shopJson)) {
            //2.如果缓存存在，直接返回
            //将 JSON 字符串转换成一个 Java 对象（Java Bean），也就是我们常说的 反序列化。
            R r = JSONUtil.toBean(shopJson, type);
            return r;
        }
        //判断缓存命中是否是空值
        if (Objects.equals(shopJson, "")) {
            return null;
        }


        //3.如果缓存不存在：

        //4.实现缓存重构问题
        //4.1获取互斥锁
        String lockKey = keyPrefix + id;
        R r = null;
        try {
            boolean isLock = tryLock(lockKey);

            //4.2判断是否获取到锁

            if (!isLock) {
                //如果获取不到锁，则等待 从redis中查询商铺缓存

                Thread.sleep(50);
                return queryWithMutex(keyPrefix, id, type, dbFallback, Time, unit);
            }
            //如果获取到锁，则执行缓存重构(查询数据库)
            //  1)查询数据库
            r = dbFallback.apply(id);
            //  a.如果商铺不存在，返回错误信息
            if (r == null) {
                //解决缓存穿透问题：商铺不存在，写入空值，也将其放入redis中
                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL + expire, TimeUnit.MINUTES);
                return null;
            }
            //  b.如果商铺存在，将商铺放入redis缓存中
            //将 Java 对象（Java Bean）转换成 JSON 字符串，也就是我们常说的序列化。
            this.set(key, r, Time, unit);

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //5.释放锁
            unLock(lockKey);
        }
        //  2)返回商铺信息
        return r;
    }










    public boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.MINUTES);
        return BooleanUtil.isTrue(flag);
    }

    public void unLock(String key) {
        stringRedisTemplate.delete(key);
    }
}
